Skip to content

[WasmFS] Make the Node backend optional for web builds#26608

Merged
kleisauke merged 8 commits intoemscripten-core:mainfrom
kleisauke:wasmfs-allow-node-backend-on-web
Apr 4, 2026
Merged

[WasmFS] Make the Node backend optional for web builds#26608
kleisauke merged 8 commits intoemscripten-core:mainfrom
kleisauke:wasmfs-allow-node-backend-on-web

Conversation

@kleisauke
Copy link
Copy Markdown
Collaborator

Previously, the WasmFS Node backend assumed Node.js was always available. This change allows building binaries that include Node backend stubs, so they can run in web environments.

Split out from #24733.

Previously, the WasmFS Node backend assumed Node.js was always
available. This change allows building binaries that include
Node backend stubs, so they can run in web environments.
_wasmfs_node_read: (fd, buf_p, len, pos, nread_p) => {},
_wasmfs_node_write: (fd, buf_p, len, pos, nwritten_p) => {},
#else
$wasmfsNodeIsWindows: "!!process.platform.match(/^win/)",
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a small drive-by fix. Previously, this would emit (when building on Linux):

var wasmfsNodeIsWindows = false;

in the JS output, which breaks when deploying these binaries on Windows.

As possible follow-up, we could also do:

Suggested change
$wasmfsNodeIsWindows: "!!process.platform.match(/^win/)",
$wasmfsNodeIsWindows: "!!globalThis.process?.platform.match(/^win/)",

This would allow a -sENVIRONMENT=node,web build to work on both Node.js and the web, using the same pattern as in other.test_wasmfs_nodefs_stubs, i.e.:

#include <emscripten/emscripten.h>
#include <emscripten/wasmfs.h>

EM_JS(bool, is_node, (), { return ENVIRONMENT_IS_NODE; });

backend_t wasmfs_create_root_dir() {
  return is_node() ? wasmfs_create_node_backend("")
                   : wasmfs_create_memory_backend();
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... PR #26624 addresses this.

_wasmfs_node_readlink: (path_p, target_p, bufsize) => {},
_wasmfs_node_close: (fd) => {},
_wasmfs_node_read: (fd, buf_p, len, pos, nread_p) => {},
_wasmfs_node_write: (fd, buf_p, len, pos, nwritten_p) => {},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these assert/abort or return non-zero?

Returning nothing/undefined here means they implicitly return zero to wasm

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! Done (and simplified) with commit d30f7de.

@@ -5,7 +5,26 @@
*/
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about simply not allowing this file to be included at all it !ENVIRONMENT_MAY_BE_NODE?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would lead to linker failures. For example, if I remove all these stubs and run ./test/runner other.test_wasmfs_nodefs_stubs, I see:

Details
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_fstat_size
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_stat_size
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_open
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_open
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_close
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_close
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_read
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_write
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_ftruncate
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_truncate
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_get_mode
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_insert_file
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_insert_directory
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_symlink
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_rename
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_unlink
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_rmdir
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_readdir
wasm-ld: error: /home/kleisauke/emscripten/cache/sysroot/lib/wasm32-emscripten/libwasmfs-debug.a(node_backend.o): undefined symbol: _wasmfs_node_readlink
...

I also thought about moving these stubs to native code and using weak linkage (like how wasmfs_create_root_dir() is handled), but this seemed easier.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I guess we should abort/assert in debug builds there though.

'}';
});

library[x] = eval(`(${t})`);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is eval needed here? I don't think we normally use eval in combination with modifyJSFunction

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied-pasted this from wrapSyscallFunction():

library[x] = eval(`(${t})`);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... but you're right, it seems unnecessary. Fixed with commit 4143136.

delete library[x + '__deps'];
t = modifyJSFunction(t, (args, body) => {
return `(${args}) => {\n` +
(ASSERTIONS ? "abort('wasmfs::NodeBackend is no-op when !ENVIRONMENT_MAY_BE_NODE');\n" : '') +
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about just abort('attempt to call nodejs backend function without ENVIRONMENT_MAY_BE_NODE)`.

I think the abort should exist even in release builds (you can make the error message optional if you like) since we never actually want a no-op behaviour here do we?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit eea0053 adjusts the error message.

I tried enabling abort() in release builds, but the wasmImports for these functions are somehow being DCE'd when building other.test_wasmfs_nodefs_stubs with -sASSERTIONS=0 or -O3, which is unexpected. I'll investigate.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... it was able to DCE away these functions as main() was empty, commit 4b86c1c fixes this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding abort() in release builds, perhaps that should be done as follow-up? I also noticed this in wrapSyscallFunction():

emscripten/src/lib/libcore.js

Lines 2514 to 2524 in 34f6bcf

// If a syscall uses FS, but !SYSCALLS_REQUIRE_FILESYSTEM, then the user
// has disabled the filesystem or we have proven some other way that this will
// not be called in practice, and do not need that code.
if (!SYSCALLS_REQUIRE_FILESYSTEM && t.includes('FS.')) {
library[x + '__deps'] = [];
t = modifyJSFunction(t, (args, body) => {
return `(${args}) => {\n` +
(ASSERTIONS ? "abort('it should not be possible to operate on streams when !SYSCALLS_REQUIRE_FILESYSTEM');\n" : '') +
'}';
});
}

Copy link
Copy Markdown
Collaborator

@sbc100 sbc100 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm % nits

'}';
});

library[x] = t;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just combine this with the line above?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


addToLibrary({
$wasmfsNodeIsWindows: !!process.platform.match(/^win/),
var WasmFSNodeLibrary = {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS names start with lowercase.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This avoids filesystem related functions from being DCE'd when
building with `-sASSERTIONS=0` or `-O3`.
return 0;
}
''')
self.run_process([EMCC, 'main.c', '-sWASMFS', '-sENVIRONMENT=web'])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what this test is doing exactly. Its not actually running the result so what is it supposed to show?

Also, when -sENVIRONMENT=web I think ENVIRONMENT_IS_NODE not even be declared/defined so this program would just crash with undefined reference I think?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a test for the linker issue mentioned in comment #26608 (comment), but since commit d30f7de it's probably no longer relevant, as we can now be sure that these stubs are always in sync.

Commit d0c6c63 removes this test.

Also, when -sENVIRONMENT=web I think ENVIRONMENT_IS_NODE not even be declared/defined so this program would just crash with undefined reference I think?

I just checked this, but ENVIRONMENT_IS_NODE is also defined in -sENVIRONMENT=web builds.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... PR #26624 reinstates this test in test_browser.py.

self.do_runf('access.c', cflags=['-sNODERAWFS'], args=[os.path.abspath('foo')])

def test_wasmfs_nodefs_stubs(self):
# This is equivalent to building with `-sWASMFS -sNODERAWFS`, except that the Wasm
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I under stand that the This is here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should probably be part of the wasmfs_create_root_dir() hook.

It became superfluous after commit d30f7de.
Copy link
Copy Markdown
Collaborator

@sbc100 sbc100 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm but maybe wait until #26609 lands.

@kleisauke kleisauke merged commit be9f95b into emscripten-core:main Apr 4, 2026
38 checks passed
@kleisauke kleisauke deleted the wasmfs-allow-node-backend-on-web branch April 4, 2026 09:52
kleisauke added a commit that referenced this pull request Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants